/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package membership

import (
	"encoding/pem"
	"strings"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/verifier"
	core "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/core"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/security/iam"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/security/iam/cspiam"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/security/iam/mgmt"
	x509GM "github.com/tjfoc/gmsm/x509"

	mb "git.cloud.inspur.com/ichain/ichain-protos-go/iam"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/logging"
	"github.com/golang/protobuf/proto"
	"github.com/pkg/errors"
)

var logger = logging.NewLogger("ichainsdk/ichain")

type identityImpl struct {
	iamManager iam.IAMManager
	iams       []string
}

// Context holds the providers
type Context struct {
	core.Providers
	EndpointConfig ichain.EndpointConfig
}

// New member identity
func New(ctx Context, cfg ichain.ChainCfg) (ichain.ChainMembership, error) {
	iamManager, iamNames, err := createIAMManager(ctx, cfg)
	if err != nil {
		return nil, err
	}
	return &identityImpl{iamManager: iamManager, iams: iamNames}, nil
}

func (i *identityImpl) Validate(serializedID []byte) error {
	err := areCertDatesValid(serializedID)
	if err != nil {
		logger.Errorf("Cert error %s", err)
		return err
	}

	id, err := i.iamManager.DeserializeIdentity(serializedID)
	if err != nil {
		logger.Errorf("failed to deserialize identity: %s", err)
		return err
	}
	return id.Validate()
}

func (i *identityImpl) Verify(serializedID []byte, msg []byte, sig []byte) error {
	id, err := i.iamManager.DeserializeIdentity(serializedID)
	if err != nil {
		return err
	}

	return id.Verify(msg, sig)
}

func (i *identityImpl) ContainsIAM(iam string) bool {
	for _, v := range i.iams {
		if v == strings.ToLower(iam) {
			return true
		}
	}
	return false
}

func areCertDatesValid(serializedID []byte) error {

	sID := &mb.SerializedIdentity{}
	err := proto.Unmarshal(serializedID, sID)
	if err != nil {
		return errors.Wrap(err, "could not deserialize a SerializedIdentity")
	}

	bl, _ := pem.Decode(sID.IdBytes)
	if bl == nil {
		return errors.New("could not decode the PEM structure")
	}
	cert, err := x509GM.ParseCertificate(bl.Bytes)
	if err != nil {
		return err
	}
	err = verifier.ValidateCertificateDates(cert)
	if err != nil {
		logger.Warnf("Certificate error '%s' for cert '%v'", err, cert.SerialNumber)
		return err
	}
	return nil
}

func createIAMManager(ctx Context, cfg ichain.ChainCfg) (iam.IAMManager, []string, error) {
	iamManager := mgmt.NewIAMManager()
	var iamNames []string
	if len(cfg.IAMs()) > 0 {
		iams, err := loadIAMs(cfg.IAMs(), ctx.CryptoSuite())
		if err != nil {
			return nil, nil, errors.WithMessage(err, "load IAMs from config failed")
		}

		if err := iamManager.Setup(iams); err != nil {
			return nil, nil, errors.WithMessage(err, "IAMManager Setup failed")
		}

		certsByIam := make(map[string][][]byte)
		for _, iam := range iams {
			iamName := iam.GetIdentifier()
			if err != nil {
				return nil, nil, errors.WithMessage(err, "IAMManager certpool setup failed")
			}
			certsByIam[iamName] = append(iam.GetTLSRootCerts(), iam.GetTLSIntermediateCerts()...)
		}

		for iamName, certs := range certsByIam {
			addCertsToConfig(ctx.EndpointConfig, certs)
			iamNames = append(iamNames, strings.ToLower(iamName))
		}
	}

	_, err := ctx.EndpointConfig.TLSCACertPool().Get()
	if err != nil {
		return nil, nil, err
	}

	return iamManager, iamNames, nil
}

func loadIAMs(iamConfigs []*mb.IAMConfig, cs core.CryptoSuite) ([]iam.IAM, error) {
	logger.Debugf("loadIAMs - start number of iams=%d", len(iamConfigs))

	iams := []iam.IAM{}
	for _, config := range iamConfigs {
		iamType := iam.ProviderType(config.Type)
		if iamType != iam.ICHAIN {
			return nil, errors.Errorf("IAM type not supported: %v", iamType)
		}
		if len(config.Config) == 0 {
			return nil, errors.Errorf("IAM configuration missing the payload in the 'Config' property")
		}

		/*ichainConfig, err := getIChainConfig(config)
		if err != nil {
			return nil, err
		}

		// get the application org names
		orgUnits := ichainConfig.OrganizationalUnitIdentifiers
		for _, orgUnit := range orgUnits {
			logger.Debugf("loadIAMs - found org of :: %s", orgUnit.OrganizationalUnitIdentifier)
		}*/

		newIAM, err := cspiam.NewCSPIAM(iam.IAMv1_0, cs)
		if err != nil {
			return nil, errors.Wrap(err, "instantiate IAM failed")
		}

		if err := newIAM.Setup(config); err != nil {
			return nil, errors.Wrap(err, "configure IAM failed")
		}

		iamID := newIAM.GetIdentifier()
		logger.Debugf("loadIAMs - adding iam=%s", iamID)

		iams = append(iams, newIAM)
	}

	logger.Debugf("loadIAMs - loaded %d IAMs", len(iams))
	return iams, nil
}

func getIChainConfig(config *mb.IAMConfig) (*mb.IchainIAMConfig, error) {

	ichainConfig := &mb.IchainIAMConfig{}
	err := proto.Unmarshal(config.Config, ichainConfig)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal ICHAINIAMConfig from config failed")
	}

	if ichainConfig.Name == "" {
		return nil, errors.New("IAM Configuration missing name")
	}

	// with this method we are only dealing with verifying IAMs, not local IAMs. Local IAMs are instantiated
	// from user enrollment materials (see User class). For verifying IAMs the root certificates are always
	// required
	if len(ichainConfig.RootCerts) == 0 {
		return nil, errors.New("IAM Configuration missing root certificates required for validating signing certificates")
	}

	return ichainConfig, nil
}

// addCertsToConfig adds cert bytes to config TLSCACertPool
func addCertsToConfig(config ichain.EndpointConfig, pemCertsList [][]byte) {

	if len(pemCertsList) == 0 {
		return
	}

	var certs []*x509GM.Certificate

	for _, pemCerts := range pemCertsList {
		for len(pemCerts) > 0 {
			var block *pem.Block
			block, pemCerts = pem.Decode(pemCerts)
			if block == nil {
				break
			}
			if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
				continue
			}

			cert, err := x509GM.ParseCertificate(block.Bytes)
			if err != nil {
				logger.Warnf("parse cert fail %s", err.Error())
				continue
			}
			certs = append(certs, cert)
		}
	}
	config.TLSCACertPool().Add(certs...)
}
